<?php

namespace Laminas\Validator\Barcode;

use Laminas\Stdlib\StringUtils;
use Laminas\Stdlib\StringWrapper\StringWrapperInterface;
use Laminas\Validator\Exception;

use function chr;
use function is_string;
use function ord;

class Code128 extends AbstractAdapter
{
    /**
     * The used string wrapper used for basic UTF-8 string functions
     *
     * @var StringWrapperInterface
     */
    protected $utf8StringWrapper;

    /**
     * Constructor for this barcode adapter
     */
    public function __construct()
    {
        $this->setLength(-1);
        $this->setCharacters([
            'A' => [
                0   => ' ',
                1   => '!',
                2   => '"',
                3   => '#',
                4   => '$',
                5   => '%',
                6   => '&',
                7   => "'",
                8   => '(',
                9   => ')',
                10  => '*',
                11  => '+',
                12  => ',',
                13  => '-',
                14  => '.',
                15  => '/',
                16  => '0',
                17  => '1',
                18  => '2',
                19  => '3',
                20  => '4',
                21  => '5',
                22  => '6',
                23  => '7',
                24  => '8',
                25  => '9',
                26  => ':',
                27  => ';',
                28  => '<',
                29  => '=',
                30  => '>',
                31  => '?',
                32  => '@',
                33  => 'A',
                34  => 'B',
                35  => 'C',
                36  => 'D',
                37  => 'E',
                38  => 'F',
                39  => 'G',
                40  => 'H',
                41  => 'I',
                42  => 'J',
                43  => 'K',
                44  => 'L',
                45  => 'M',
                46  => 'N',
                47  => 'O',
                48  => 'P',
                49  => 'Q',
                50  => 'R',
                51  => 'S',
                52  => 'T',
                53  => 'U',
                54  => 'V',
                55  => 'W',
                56  => 'X',
                57  => 'Y',
                58  => 'Z',
                59  => '[',
                60  => '\\',
                61  => ']',
                62  => '^',
                63  => '_',
                64  => 0x00,
                65  => 0x01,
                66  => 0x02,
                67  => 0x03,
                68  => 0x04,
                69  => 0x05,
                70  => 0x06,
                71  => 0x07,
                72  => 0x08,
                73  => 0x09,
                74  => 0x0A,
                75  => 0x0B,
                76  => 0x0C,
                77  => 0x0D,
                78  => 0x0E,
                79  => 0x0F,
                80  => 0x10,
                81  => 0x11,
                82  => 0x12,
                83  => 0x13,
                84  => 0x14,
                85  => 0x15,
                86  => 0x16,
                87  => 0x17,
                88  => 0x18,
                89  => 0x19,
                90  => 0x1A,
                91  => 0x1B,
                92  => 0x1C,
                93  => 0x1D,
                94  => 0x1E,
                95  => 0x1F,
                96  => 'Ç',
                97  => 'ü',
                98  => 'é',
                99  => 'â',
                100 => 'ä',
                101 => 'à',
                102 => 'å',
                103 => '‡',
                104 => 'ˆ',
                105 => '‰',
                106 => 'Š',
            ],
            'B' => [
                0   => ' ',
                1   => '!',
                2   => '"',
                3   => '#',
                4   => '$',
                5   => '%',
                6   => '&',
                7   => "'",
                8   => '(',
                9   => ')',
                10  => '*',
                11  => '+',
                12  => ',',
                13  => '-',
                14  => '.',
                15  => '/',
                16  => '0',
                17  => '1',
                18  => '2',
                19  => '3',
                20  => '4',
                21  => '5',
                22  => '6',
                23  => '7',
                24  => '8',
                25  => '9',
                26  => ':',
                27  => ';',
                28  => '<',
                29  => '=',
                30  => '>',
                31  => '?',
                32  => '@',
                33  => 'A',
                34  => 'B',
                35  => 'C',
                36  => 'D',
                37  => 'E',
                38  => 'F',
                39  => 'G',
                40  => 'H',
                41  => 'I',
                42  => 'J',
                43  => 'K',
                44  => 'L',
                45  => 'M',
                46  => 'N',
                47  => 'O',
                48  => 'P',
                49  => 'Q',
                50  => 'R',
                51  => 'S',
                52  => 'T',
                53  => 'U',
                54  => 'V',
                55  => 'W',
                56  => 'X',
                57  => 'Y',
                58  => 'Z',
                59  => '[',
                60  => '\\',
                61  => ']',
                62  => '^',
                63  => '_',
                64  => '`',
                65  => 'a',
                66  => 'b',
                67  => 'c',
                68  => 'd',
                69  => 'e',
                70  => 'f',
                71  => 'g',
                72  => 'h',
                73  => 'i',
                74  => 'j',
                75  => 'k',
                76  => 'l',
                77  => 'm',
                78  => 'n',
                79  => 'o',
                80  => 'p',
                81  => 'q',
                82  => 'r',
                83  => 's',
                84  => 't',
                85  => 'u',
                86  => 'v',
                87  => 'w',
                88  => 'x',
                89  => 'y',
                90  => 'z',
                91  => '{',
                92  => '|',
                93  => '}',
                94  => '~',
                95  => 0x7F,
                96  => 'Ç',
                97  => 'ü',
                98  => 'é',
                99  => 'â',
                100 => 'ä',
                101 => 'à',
                102 => 'å',
                103 => '‡',
                104 => 'ˆ',
                105 => '‰',
                106 => 'Š',
            ],
            'C' => [
                0   => '00',
                1   => '01',
                2   => '02',
                3   => '03',
                4   => '04',
                5   => '05',
                6   => '06',
                7   => '07',
                8   => '08',
                9   => '09',
                10  => '10',
                11  => '11',
                12  => '12',
                13  => '13',
                14  => '14',
                15  => '15',
                16  => '16',
                17  => '17',
                18  => '18',
                19  => '19',
                20  => '20',
                21  => '21',
                22  => '22',
                23  => '23',
                24  => '24',
                25  => '25',
                26  => '26',
                27  => '27',
                28  => '28',
                29  => '29',
                30  => '30',
                31  => '31',
                32  => '32',
                33  => '33',
                34  => '34',
                35  => '35',
                36  => '36',
                37  => '37',
                38  => '38',
                39  => '39',
                40  => '40',
                41  => '41',
                42  => '42',
                43  => '43',
                44  => '44',
                45  => '45',
                46  => '46',
                47  => '47',
                48  => '48',
                49  => '49',
                50  => '50',
                51  => '51',
                52  => '52',
                53  => '53',
                54  => '54',
                55  => '55',
                56  => '56',
                57  => '57',
                58  => '58',
                59  => '59',
                60  => '60',
                61  => '61',
                62  => '62',
                63  => '63',
                64  => '64',
                65  => '65',
                66  => '66',
                67  => '67',
                68  => '68',
                69  => '69',
                70  => '70',
                71  => '71',
                72  => '72',
                73  => '73',
                74  => '74',
                75  => '75',
                76  => '76',
                77  => '77',
                78  => '78',
                79  => '79',
                80  => '80',
                81  => '81',
                82  => '82',
                83  => '83',
                84  => '84',
                85  => '85',
                86  => '86',
                87  => '87',
                88  => '88',
                89  => '89',
                90  => '90',
                91  => '91',
                92  => '92',
                93  => '93',
                94  => '94',
                95  => '95',
                96  => '96',
                97  => '97',
                98  => '98',
                99  => '99',
                100 => 'ä',
                101 => 'à',
                102 => 'å',
                103 => '‡',
                104 => 'ˆ',
                105 => '‰',
                106 => 'Š',
            ],
        ]);
        $this->setChecksum('code128');
    }

    /**
     * @return void
     */
    public function setUtf8StringWrapper(StringWrapperInterface $utf8StringWrapper)
    {
        if (! $utf8StringWrapper->isSupported('UTF-8')) {
            throw new Exception\InvalidArgumentException(
                'The string wrapper needs to support UTF-8 character encoding'
            );
        }
        $this->utf8StringWrapper = $utf8StringWrapper;
    }

    /**
     * Get the string wrapper supporting UTF-8 character encoding
     *
     * @return StringWrapperInterface
     */
    public function getUtf8StringWrapper()
    {
        if (! $this->utf8StringWrapper) {
            $this->utf8StringWrapper = StringUtils::getWrapper('UTF-8');
        }
        return $this->utf8StringWrapper;
    }

    /**
     * Checks for allowed characters within the barcode
     *
     * @param  string $value The barcode to check for allowed characters
     * @return bool
     */
    public function hasValidCharacters($value)
    {
        if (! is_string($value)) {
            return false;
        }

        // get used string wrapper for UTF-8 character encoding
        $strWrapper = $this->getUtf8StringWrapper();

        // detect starting charset
        $set  = $this->getCodingSet($value);
        $read = $set;
        if ($set !== '') {
            $value = $strWrapper->substr($value, 1, null);
        }

        // process barcode
        while ($value !== '' && $value !== false) {
            $char = $strWrapper->substr($value, 0, 1);

            switch ($char) {
                // Function definition
                case 'Ç':
                case 'ü':
                case 'å':
                    break;

                // Switch 1 char between A and B
                case 'é':
                    if ($set === 'A') {
                        $read = 'B';
                        break;
                    }

                    if ($set === 'B') {
                        $read = 'A';
                        break;
                    }

                    break;

                // Switch to C
                case 'â':
                    $set  = 'C';
                    $read = 'C';
                    break;

                // Switch to B
                case 'ä':
                    $set  = 'B';
                    $read = 'B';
                    break;

                // Switch to A
                case 'à':
                    $set  = 'A';
                    $read = 'A';
                    break;

                // Doubled start character
                case '‡':
                case 'ˆ':
                case '‰':
                    return false;

                // Chars after the stop character
                case 'Š':
                    break 2;

                default:
                    // Does the char exist within the charset to read?
                    if ($this->ord128($char, $read) === -1) {
                        return false;
                    }

                    break;
            }

            $value = $strWrapper->substr($value, 1, null);
            $read  = $set;
        }

        if ($value !== '' && is_string($value) && $strWrapper->strlen($value) !== 1) {
            return false;
        }

        return true;
    }

    /**
     * Validates the checksum ()
     *
     * @param  string $value The barcode to validate
     * @return bool
     */
    protected function code128($value)
    {
        $sum        = 0;
        $pos        = 1;
        $set        = $this->getCodingSet($value);
        $read       = $set;
        $usecheck   = $this->useChecksum(null);
        $strWrapper = $this->getUtf8StringWrapper();
        $char       = $strWrapper->substr($value, 0, 1);
        if ($char === '‡') {
            $sum = 103;
        } elseif ($char === 'ˆ') {
            $sum = 104;
        } elseif ($char === '‰') {
            $sum = 105;
        } elseif ($usecheck === true) {
            // no start value, unable to detect a proper checksum
            return false;
        }

        $value = $strWrapper->substr($value, 1, null);
        while ($strWrapper->strpos($value, 'Š') || ($value !== '')) {
            $char = $strWrapper->substr($value, 0, 1);
            if ($read === 'C') {
                $char = $strWrapper->substr($value, 0, 2);
            }

            switch ($char) {
                // Function definition
                case 'Ç':
                case 'ü':
                case 'å':
                    $sum += $pos * $this->ord128($char, $set);
                    break;

                case 'é':
                    $sum += $pos * $this->ord128($char, $set);

                    if ($set === 'A') {
                        $read = 'B';
                        break;
                    }

                    if ($set === 'B') {
                        $read = 'A';
                        break;
                    }

                    break;

                // Switch to C
                case 'â':
                    $sum += $pos * $this->ord128($char, $set);
                    $set  = 'C';
                    $read = 'C';
                    break;

                // Switch to B
                case 'ä':
                    $sum += $pos * $this->ord128($char, $set);
                    $set  = 'B';
                    $read = 'B';
                    break;

                // Switch to A
                case 'à':
                    $sum += $pos * $this->ord128($char, $set);
                    $set  = 'A';
                    $read = 'A';
                    break;

                case '‡':
                case 'ˆ':
                case '‰':
                    return false;

                default:
                    // Does the char exist within the charset to read?
                    if ($this->ord128($char, $read) === -1) {
                        return false;
                    }

                    $sum += $pos * $this->ord128($char, $set);
                    break;
            }

            $value = $strWrapper->substr($value, 1);
            ++$pos;
            if (($strWrapper->strpos($value, 'Š') === 1) && ($strWrapper->strlen($value) === 2)) {
                // break by stop and checksum char
                break;
            }
            $read = $set;
        }

        if (($strWrapper->strpos($value, 'Š') !== 1) || ($strWrapper->strlen($value) !== 2)) {
            // return false if checksum is not readable and true if no startvalue is detected
            return ! $usecheck;
        }

        $mod = $sum % 103;
        if ($strWrapper->substr($value, 0, 1) === $this->chr128($mod, $set)) {
            return true;
        }

        return false;
    }

    /**
     * Returns the coding set for a barcode
     *
     * @param string $value Barcode
     * @return string
     */
    protected function getCodingSet($value)
    {
        $value = $this->getUtf8StringWrapper()->substr($value, 0, 1);
        return match ($value) {
            '‡' => 'A',
            'ˆ' => 'B',
            '‰' => 'C',
            default => '',
        };
    }

    /**
     * Internal method to return the code128 integer from an ascii value
     *
     * Table A
     *    ASCII       CODE128
     *  32 to  95 ==  0 to  63
     *   0 to  31 == 64 to  95
     * 128 to 138 == 96 to 106
     *
     * Table B
     *    ASCII       CODE128
     *  32 to 138 == 0 to 106
     *
     * Table C
     *    ASCII       CODE128
     *  "00" to "99" ==   0 to  99
     *   132 to  138 == 100 to 106
     *
     * @param string $value
     * @param string $set
     * @return int
     */
    protected function ord128($value, $set)
    {
        $ord = ord($value);
        if ($set === 'A') {
            if ($ord < 32) {
                return $ord + 64;
            } elseif ($ord < 96) {
                return $ord - 32;
            } elseif ($ord > 138) {
                return -1;
            } else {
                return $ord - 32;
            }
        } elseif ($set === 'B') {
            if ($ord < 32) {
                return -1;
            } elseif ($ord <= 138) {
                return $ord - 32;
            } else {
                return -1;
            }
        } elseif ($set === 'C') {
            $val = (int) $value;
            if (($val >= 0) && ($val <= 99)) {
                return $val;
            } elseif (($ord >= 132) && ($ord <= 138)) {
                return $ord - 32;
            } else {
                return -1;
            }
        } else {
            if ($ord < 32) {
                return $ord + 64;
            } elseif ($ord <= 138) {
                return $ord - 32;
            } else {
                return -1;
            }
        }
    }

    /**
     * Internal Method to return the ascii value from a code128 integer
     *
     * Table A
     *    ASCII       CODE128
     *  32 to  95 ==  0 to  63
     *   0 to  31 == 64 to  95
     * 128 to 138 == 96 to 106
     *
     * Table B
     *    ASCII       CODE128
     *  32 to 138 == 0 to 106
     *
     * Table C
     *    ASCII       CODE128
     *  "00" to "99" ==   0 to  99
     *   132 to  138 == 100 to 106
     *
     * @param int $value
     * @param string $set
     * @return int|string
     */
    protected function chr128($value, $set)
    {
        if ($set === 'A') {
            if ($value < 64) {
                return chr($value + 32);
            } elseif ($value < 96) {
                return chr($value - 64);
            } elseif ($value > 106) {
                return -1;
            } else {
                return chr($value + 32);
            }
        } elseif ($set === 'B') {
            if ($value > 106) {
                return -1;
            } else {
                return chr($value + 32);
            }
        } elseif ($set === 'C') {
            if (($value >= 0) && ($value <= 9)) {
                return '0' . (string) $value;
            } elseif ($value <= 99) {
                return (string) $value;
            } elseif ($value <= 106) {
                return chr($value + 32);
            } else {
                return -1;
            }
        } else {
            if ($value <= 106) {
                return $value + 32;
            } else {
                return -1;
            }
        }
    }
}
